package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.asm.MemberSubstitution;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author zzj
 * @since 2024-05-23
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserCouponMqServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService exchangeCodeService;
    private final RedissonClient redissonClient;
    private final StringRedisTemplate redisTemplate;
    private final RabbitMqHelper mqHelper;
    private final ICouponScopeService couponScopeService;
    private final Executor calculateSolutionExecutor;

    /**
     * @description: 领取优惠券
     * @params: [id]
     * @return:
     * @author: ajie
     * @date: 2024/5/24 15:27
     */
    @Override
    //分布式锁可以 对 优惠券加锁
    @MyLock(name = "lock:coupon:uid:#{userId}")
    public void receiveCoupon(Long id) {
        //校验id查询优惠券状态 做相关校验
        if (id == null) {
            throw new BadRequestException("非法参数");
        }
//        Coupon coupon = couponMapper.selectById(id);
        //从redis中获取优惠券信息
        Coupon coupon = queryCouponByCache(id);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
//        if (coupon.getStatus() != CouponStatus.ISSUING) {//只有发放了才会在redis中，所以不用判断
//            throw new BadRequestException("该优惠券不是正在发放中");
//        }
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("该优惠券已过期或未发放");
        }
//        if (coupon.getTotalNum() <= 0 || coupon.getIssueNum() >= coupon.getTotalNum()) {
        if (coupon.getTotalNum() <= 0) {//redis中每领一个券减一
            throw new BadRequestException("该优惠券库存不足");
        }
        Long userId = UserContext.getUser();
        /*//获取当前用户 对该优惠券 已领数量 user_coupon 条件userId couponId
        Integer count = this.lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, id)
                .count();
        if (count != null && count>=coupon.getUserLimit()){
            throw new BadRequestException("已达到领取上限");
        }
        //优惠券的已发放数量+1
//        coupon.setIssueNum(coupon.getIssueNum() + 1);
//        couponMapper.updateById(coupon);
        couponMapper.incrIssueNum(id);//采用这种方式 后期考虑并发控制 后期仍需修改

        //生成用户券
        saveUserCoupon(userId,coupon);*/
//        checkAndCreateUserCoupon(userId, coupon,null);

//        synchronized (userId.toString().intern()){
//            checkAndCreateUserCoupon(userId, coupon,null);
//        }

//        synchronized (userId.toString().intern()){
//            //从aop上下文中 获取当前类的代理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
////            checkAndCreateUserCoupon(userId, coupon,null);//这种方法是调用原对象的方法
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon,null);//这种方法调用代理对象的方法，方法是有事务处理的
//        }

//        //通过redisson实现分布式锁
//        String key = "lock:coupon:uid";
//        RLock lock = redissonClient.getLock(key);//获取锁对象
//        try {
//            boolean isLock = lock.tryLock();//没有参数，看门狗机制会生效，默认失效时间是30秒
//            if (!isLock){
//                throw new  BizIllegalException("操作太频繁了");
//            }
//            //从aop上下文中 获取当前类的代理对象
//            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
////            checkAndCreateUserCoupon(userId, coupon,null);//这种方法是调用原对象的方法
//            userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon,null);//这种方法调用代理对象的方法，方法是有事务处理的
//        }finally {
//            lock.unlock();
//        }

        //通过redisson实现分布式锁
//        String key = "lock:coupon:uid";
        //从aop上下文中 获取当前类的代理对象

        //统计已领数量
        String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + id;//prs:user:coupon:优惠券id
//        long num = redisTemplate.opsForHash().get(key, userId);
        //校验是否超出已领数量
//        if (num>=coupon.getUserLimit()) throw  xxx
        //修改已领数量+1
//        long num = redisTemplate.opsForHash().put(key,userId,num+1);

        //increment 代表本次领取后的 已领数量
        Long increment = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
        if (increment > coupon.getUserLimit()) {//由于increment是+1之后的结果，所以此处只能判断大于 不能等于
            throw new BizIllegalException("超出已领数量");
        }

        //修改优惠券的库存 -1
        String couponKey = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        redisTemplate.opsForHash().increment(couponKey, "totalNum", -1);

        //发送消息到mq 消息内容为 userId couponId   （只要能发消息，说明用户能领券）
        UserCouponDTO msg = new UserCouponDTO();
        msg.setUserId(userId);
        msg.setCouponId(id);
        mqHelper.send(
                MqConstants.Exchange.PROMOTION_EXCHANGE,
                MqConstants.Key.COUPON_RECEIVE,
                msg
        );


       /* IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
//            checkAndCreateUserCoupon(userId, coupon,null);//这种方法是调用原对象的方法
        userCouponServiceProxy.checkAndCreateUserCoupon(userId, coupon, null);//这种方法调用代理对象的方法，方法是有事务处理的
*/
    }

    /**
     * @description: 从redis中获取优惠券信息 (领取开始和结束时间、发行总数量、限领数量)
     * @params: [id]
     * @return:
     * @author: ajie
     * @date: 2024/5/27 12:48
     */
    private Coupon queryCouponByCache(Long id) {
        //拼接key
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        //从redis获取数量
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        Coupon coupon = BeanUtils.mapToBean(entries, Coupon.class, false, CopyOptions.create());
        return coupon;
    }

    @Override
    @Transactional
    public void exchangeCoupon(String code) {
        //校验code是否为空
        if (StringUtils.isBlank(code)) {
            throw new BadRequestException("非法参数");
        }
        //解析兑换码得到自增id
        long serialNum = CodeUtil.parseCode(code);//自增id
        log.debug("自增id：{}", serialNum);
        //判断兑换码是否已兑换 采用redis的bitmap结构 setbit key offset 1 如果方法返回为true代表兑换码已兑换
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialNum, true);
        if (result) {
            //说明兑换码已经被兑换
            throw new BizIllegalException("兑换码已经被使用");
        }
        try {
            //判断兑换码是否存在 根据自增id查询 主键查询
            ExchangeCode exchangeCode = exchangeCodeService.getById(serialNum);
            if (exchangeCode == null) {
                throw new BizIllegalException("兑换码不存在");
            }
            //判断是否过期
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expiredTime = exchangeCode.getExpiredTime();
            if (now.isAfter(expiredTime)) {
                throw new BizIllegalException("兑换码已过期");
            }

            Long userId = UserContext.getUser();
            //查询优惠券信息
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            if (coupon == null) {
                throw new BizIllegalException("优惠券不存在");
            }
            //校验并生成用户券
            checkAndCreateUserCoupon(userId, coupon, serialNum);

        } catch (Exception e) {
            //将兑换码状态重置
            exchangeCodeService.updateExchangeCodeMark(serialNum, false);
            throw e;
        }

    }

    @Transactional
    @MyLock(name = "lock:coupon:uid:#{userId}",
            lockType = MyLockType.RE_ENTRANT_LOCK,
            lockStrategy = MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT)
    public void checkAndCreateUserCoupon(Long userId, Coupon coupon, Long serialNum) {
        //Long类型 -128-127 之间是同一个对象 超过该区间是不同的对象
        //Long。toString 方法底层是new String，所以还是不同的对象
        //Long.toString.intern intern方法是强制从常量池中取字符串
//        synchronized (userId.toString().intern()){
        //获取当前用户 对该优惠券 已领数量 user_coupon 条件userId couponId
        Integer count = this.lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getCouponId, coupon.getId())
                .count();
        if (count != null && count >= coupon.getUserLimit()) {
            throw new BadRequestException("已达到领取上限");
        }
        //优惠券的已发放数量+1
//        coupon.setIssueNum(coupon.getIssueNum() + 1);
//        couponMapper.updateById(coupon);
        int num = couponMapper.incrIssueNum(coupon.getId());//采用这种方式 后期考虑并发控制 后期仍需修改
        if (num == 0) {
            throw new BizIllegalException("券已领完");
        }

        //生成用户券
        saveUserCoupon(userId, coupon);

        //更新兑换码状态
        if (serialNum != null) {
            //修改兑换码状态
            exchangeCodeService.lambdaUpdate()
                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                    .set(ExchangeCode::getUserId, userId)
                    .eq(ExchangeCode::getId, serialNum)
                    .update();
        }
//        }
    }

    @Transactional
    @Override
    public void checkAndCreateUserCouponNew(UserCouponDTO msg) {
        //Long类型 -128-127 之间是同一个对象 超过该区间是不同的对象
        //Long.toString 方法底层是new String，所以还是不同的对象
        //Long.toString.intern intern方法是强制从常量池中取字符串
//        synchronized (userId.toString().intern()){
        //获取当前用户 对该优惠券 已领数量 user_coupon 条件userId couponId
//        Integer count = this.lambdaQuery()
//                .eq(UserCoupon::getUserId, userId)
//                .eq(UserCoupon::getCouponId, coupon.getId())
//                .count();
//        if (count != null && count >= coupon.getUserLimit()) {
//            throw new BadRequestException("已达到领取上限");
//        }
        //优惠券的已发放数量+1
//        coupon.setIssueNum(coupon.getIssueNum() + 1);
//        couponMapper.updateById(coupon);

        //从db中查询优惠券消息
        Coupon coupon = couponMapper.selectById(msg.getCouponId());
        if (coupon == null) {
            return;
        }
        int num = couponMapper.incrIssueNum(coupon.getId());//采用这种方式 后期考虑并发控制 后期仍需修改
        if (num == 0) {
//            throw new BizIllegalException("券已领完");
            return;
        }

        //生成用户券
        saveUserCoupon(msg.getUserId(), coupon);

//        //更新兑换码状态
//        if (serialNum != null) {
//            //修改兑换码状态
//            exchangeCodeService.lambdaUpdate()
//                    .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
//                    .set(ExchangeCode::getUserId, userId)
//                    .eq(ExchangeCode::getId, serialNum)
//                    .update();
//        }
//        }
    }

    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> courses) {
        //查询当前用户优惠券 coupon和user_coupon表  条件：userid、status = 1 查哪些字段 优惠券规则 优惠券id 用户券id
        List<Coupon> coupons = getBaseMapper().queryMyCoupons(UserContext.getUser());
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }
        log.debug("用户的优惠券共有 {}张", coupons.size());
        for (Coupon coupon : coupons) {
            log.debug("优惠券：{},{}",
                    DiscountStrategy.getDiscount(coupon.getDiscountType()).getRule(coupon),
                    coupon);
        }

        //初筛
        //计算订单的总金额，对courses的price累加
        int totalAmount = courses.stream().mapToInt(OrderCourseDTO::getPrice).sum();

        //校验优惠券是否可用
        List<Coupon> availableCoupons = coupons.stream()
                .filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon))
                .collect(Collectors.toList());
        if (CollUtils.isEmpty(availableCoupons)) {
            return CollUtils.emptyList();
        }
        log.debug("经过初筛之后，还剩 {}张", availableCoupons.size());
        for (Coupon coupon : availableCoupons) {
            log.debug("优惠券：{},{}",
                    DiscountStrategy.getDiscount(coupon.getDiscountType()).getRule(coupon),
                    coupon);
        }

        //细筛（需要考虑优惠券的限定范围）排列组合
        Map<Coupon, List<OrderCourseDTO>> avaMap = findAvailableCoupons(availableCoupons, courses);
        if (avaMap.isEmpty()) {
            return CollUtils.emptyList();
        }
        Set<Map.Entry<Coupon, List<OrderCourseDTO>>> entries = avaMap.entrySet();
        for (Map.Entry<Coupon, List<OrderCourseDTO>> entry : entries) {
            log.debug("细筛之后优惠券：{},{}",
                    DiscountStrategy.getDiscount(entry.getKey().getDiscountType()).getRule(entry.getKey()),
                    entry.getKey());
            List<OrderCourseDTO> value = entry.getValue();
            for (OrderCourseDTO courseDTO : value) {
                log.debug("可用课程 {}", courseDTO);
            }
        }
        availableCoupons = new ArrayList<>(avaMap.keySet());//才是真正可用的优惠券集合
        log.debug("经过细筛之后，还剩 {}张", availableCoupons.size());
        for (Coupon coupon : availableCoupons) {
            log.debug("优惠券：{},{}",
                    DiscountStrategy.getDiscount(coupon.getDiscountType()).getRule(coupon),
                    coupon);
        }
        //排列组合
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons);
        for (Coupon availableCoupon : availableCoupons) {
            solutions.add(List.of(availableCoupon));//添加单券到方案中
        }
        log.debug("排列组合");
        for (List<Coupon> solution : solutions) {
            List<Long> cids = solution.stream().map(Coupon::getId).collect(Collectors.toList());
            log.debug("{}", cids);
        }

        //计算每一种组合的优惠明细
//        log.debug("开始计算每一种优惠的明细");
//        List<CouponDiscountDTO> dtos = new ArrayList<>();
//        for (List<Coupon> solution : solutions) {
//            CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, courses, solution);
//            log.debug("方案最终优惠：{}   方案中优惠券使用了：{}    规则：{}", dto.getDiscountAmount(), dto.getIds(), dto.getRules());
//            dtos.add(dto);
//        }

        //使用多线程改造 并行计算每一种组合的优惠情况
        log.debug("多线程计算 每一种组合的优惠情况");
//        List<CouponDiscountDTO> dtos = new ArrayList<>();//线程不安全
        List<CouponDiscountDTO> dtos = Collections.synchronizedList(new ArrayList<>(solutions.size()));//线程安全的集合
        CountDownLatch latch = new CountDownLatch(solutions.size());//多少种方案开启多少个线程
        for (List<Coupon> solution : solutions) {
            CompletableFuture.supplyAsync(() -> {
                log.debug("线程：{}    开始计算方案：{}",
                        Thread.currentThread().getName(),
                        solution.stream().map(Coupon::getId).collect(Collectors.toSet()));
                CouponDiscountDTO dto = calculateSolutionDiscount(avaMap, courses, solution);
                return dto;
            },calculateSolutionExecutor).thenAccept(dto -> {
                log.debug("方案最终优惠：{}   方案中优惠券使用了：{}    规则：{}", dto.getDiscountAmount(), dto.getIds(), dto.getRules());
                dtos.add(dto);
                latch.countDown();//计算器减一
            });
        }
        try {
            latch.await(2, TimeUnit.SECONDS);//主线程最多阻塞2秒
        } catch (InterruptedException e) {
            log.error("多线程计算组合优惠明细 报错了",e);
        }

        //筛选最优解
        return findBestSolution(dtos);
    }

    /**
     * @description: 求最优解
     *  - 用券相同时，优惠金额最高的方案
     *  - 优惠金额相同时，用券最少的方案
     * @params: [solutions]
     * @return:
     * @author: ajie
     * @date: 2024/5/31 15:07
     */
    private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> solutions) {
        //1. 创建两个map 分别记录用券相同，金额最高，   金额相同，用券最少
        Map<String,CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        Map<Integer,CouponDiscountDTO> lessCouponMap = new HashMap<>();

        //2. 循环方案 向map中记录 用券相同，金额最高，   金额相同，用券最少
        for (CouponDiscountDTO solution : solutions) {
            //2.1 对优惠券id升序，转字符串 然后以逗号拼接
            String ids = solution.getIds().stream().sorted(Comparator.comparing(Long::longValue)).map(String::valueOf).collect(Collectors.joining(","));
            //2.2 从moreDiscountMap中取 旧的记录 判断 旧的金额方案 大于等于 当前方案的优惠金额 当前方案忽略，处理下一个方案
            CouponDiscountDTO old = moreDiscountMap.get(ids);
            if (old != null && old.getDiscountAmount() >= solution.getDiscountAmount()){
                continue;
            }
            //2.3 从lessCouponMap中取 旧的记录 判断 旧的方案用券数量 小于等于 当前方案用券数量 当前方案忽略，处理下一个方案
            old = lessCouponMap.get(solution.getDiscountAmount());
            int newSize = solution.getIds().size();//当前方案的用券数量
            if (old != null && newSize > 1 && old.getIds().size() <= newSize){//单券忽略不考虑
                continue;
            }
            //2.4 添加 更优方案 到 map中
            moreDiscountMap.put(ids,solution);//说明当前方案 更优
            lessCouponMap.put(solution.getDiscountAmount(), solution);//说明当前方案 更优
        }

        //3. 求两个map的交集
        Collection<CouponDiscountDTO> bestSolution = CollUtils.intersection(moreDiscountMap.values(), lessCouponMap.values());

        //4. 对最终的方案结果 按优惠金额 倒序
        List<CouponDiscountDTO> latestBestSolution = bestSolution.stream()
                .sorted(Comparator.comparing(CouponDiscountDTO::getDiscountAmount).reversed())
                .collect(Collectors.toList());
        return latestBestSolution;
    }

    /**
     * @description: 计算每一个方案的优惠信息
     * @params: [avaMap 优惠券和可用课程的映射集合, courses 订单中的所有课程, solution 方案]
     * @return:
     * @author: ajie
     * @date: 2024/5/29 9:53
     */
    private CouponDiscountDTO calculateSolutionDiscount(Map<Coupon, List<OrderCourseDTO>> avaMap,
                                                        List<OrderCourseDTO> courses,
                                                        List<Coupon> solution) {
        //创建方案结果dto对象
        CouponDiscountDTO dto = new CouponDiscountDTO();
        //初始化商品id和商品折扣明细的映射，  初始折扣明细全部设置为0 设置map结构，key为商品id，value初始值都为0
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, orderCourseDTO -> 0));
        //计算该方案的优惠信息
        //循环方案中的优惠券
        for (Coupon coupon : solution) {
            //取出优惠券对应的可用的课程
            List<OrderCourseDTO> availableCourses = avaMap.get(coupon);
            //计算可用课程的总金额（商品价格 - 该商品的折扣明细）
            int totalAmount = availableCourses.stream()
                    .mapToInt(value -> value.getPrice() - detailMap.get(value.getId())).sum();
            //判断优惠券是否可用
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (!discount.canUse(totalAmount, coupon)) {
                continue;//券不可用，跳出循环，继续下一个券
            }
            //计算该优惠券使用之后的折扣值
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            //计算商品的折扣明细 更新到detailsMap
            calculateDetailDiscount(detailMap, availableCourses, totalAmount, discountAmount);
            //累加每一个优惠券的优惠金额 赋值给方案结果dto对象
            dto.getIds().add(coupon.getId());//只要执行到当前这句话，就意味着这个优惠券生效了
            dto.getRules().add(discount.getRule(coupon));
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());//不能覆盖应该是所有生效优惠券的累加结果
        }
        return dto;
    }

    /**
     * @description: 计算商品 折扣明细
     * @params: [detailMap 商品id和商品的优惠明细 映射, availableCourses 当前优惠券可用的课程集合
     * ,         totalAmount 可用的课程总金额, discountAmount 当前优惠券能优惠的金额]
     * @return:
     * @author: ajie
     * @date: 2024/5/29 20:02
     */
    private void calculateDetailDiscount(Map<Long, Integer> detailMap,
                                         List<OrderCourseDTO> availableCourses,
                                         int totalAmount,
                                         int discountAmount) {
        //目的：本方法就是优惠券在使用后 计算每个商品的折扣明细
        //规划：前面的商品按比例计算，最后一个商品折扣明细 = 总的优惠金额 - 前面商品优惠的总额
        //循环可用商品
        int time = 0;//代表已处理的商品个数
        int remainDiscount = discountAmount;//代表剩余的优惠金额

        for (OrderCourseDTO c : availableCourses) {
            time++;
            int discount = 0;
            if (time == availableCourses.size()) {
                //说明是最后一个课程
                discount = remainDiscount;
            } else {
                //是前面的课程 按比例
                discount = c.getPrice() + discountAmount / totalAmount;// 此处先乘后除 否则结果是0
                remainDiscount = remainDiscount - discount;
            }
            //将商品的折扣明细添加到 detailMap 累加
            detailMap.put(c.getId(), discount + detailMap.get(c.getId()));
        }
    }

    /**
     * @description: 细筛，查询每一个优惠券 对应的可用课程
     * @params: [coupons 初筛之后的优惠券集合, orderCourse 订单中的课程集合]
     * @return:
     * @author: ajie
     * @date: 2024/5/28 19:57
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons, List<OrderCourseDTO> orderCourses) {
        Map<Coupon, List<OrderCourseDTO>> map = new HashMap<>();
        //循环遍历初筛之后的优惠券集合
        for (Coupon coupon : coupons) {
            //找出每一个优惠券的可用课程
            List<OrderCourseDTO> availableCourses = orderCourses;
            //判断优惠券是否限定了范围 coupon.specific为true
            if (coupon.getSpecific()) {
                //查询限定范围 查询coupon_scope表，条件coupon_id
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                //得到限定范围的id集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                //从 orderCourses 订单中所有的课程集合 筛选 该范围内的课程
                availableCourses = orderCourses.stream()
                        .filter(orderCourseDTO -> scopeIds.contains(orderCourseDTO.getCateId()))
                        .collect(Collectors.toList());
            }
            if (CollUtils.isEmpty(availableCourses)) {
                continue;//说明当前优惠券限定了范围，但是在订单课程中没有找到可用的课程，说明该券不可用，忽略该券，进行下一个优惠券的处理
            }

            //计算该优惠券 可用课程的总金额
            int totalAmount = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();//代表可用课程的总金额

            //判断该优惠券是否可用 如果可用添加到map中
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if (discount.canUse(totalAmount, coupon)) {
                map.put(coupon, availableCourses);
            }
        }
        return map;
    }

    /**
     * @description: 保存优惠券
     * @params: [userId, coupon]
     * @return:
     * @author: ajie
     * @date: 2024/5/24 16:05
     */
    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId);
        userCoupon.setCouponId(coupon.getId());
        LocalDateTime termBeginTime = coupon.getTermBeginTime();//优惠券使用 开始时间
        LocalDateTime termEndTime = coupon.getTermEndTime();//优惠券使用 截止时间
        if (termBeginTime == null && termEndTime == null) {
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        userCoupon.setTermBeginTime(termBeginTime);
        userCoupon.setTermEndTime(termEndTime);
        this.save(userCoupon);
    }
}
